⚠️ Roteiro para executar códigos do notebook no Google Colab(oratory) com GPU_

PASSO 1: Acessar https://colab.research.google.com/ $\to$ Upload $\to$ escolher arquivo .ipynb

PASSO 2: Acessar MENU Ambiente de execução $\to$ Alterar tipo do ambiente $\to$ GPU $\to$ Salvar

PASSO 3: Barra lateral esquerda $\to$ Icone de arquivo $\to$ Icone de upload $\to$ escolher arquivo auxiliar (ex. aux.py)

PASSOS 4, 5, 6 e 7: ver células a seguir...

Limitações da MLP...

1. ENTRADA: Smartphone 12MP $\implies$ Imagem com 36M de características (RGB)
Se HIDDEN = 100 neurônios $\implies$ 3,6 bilhões de parâmetros! $\implies$ 14GB 😳

2. MLP é invariante à ordem das características $\implies$ resultados semelhantes mesmo não respeitando ordenação da estrutura espacial dos pixels.

Invariância espacial

fig01.jpeg

A aparência de Wally não depende de sua localização. Podemos criar um detector que faça uma varredura da cena, atribuindo uma pontuação a cada padrão observado, indicando a probabilidade de que o padrão contenha o Wally.

Dois princípios relacionados:

  1. Invariância translacional: nosso detector deve ter capacidade de reconhecer o padrÃo independentemente de onde ele apareça na imagem.
  2. Localidade: não deveríamos precisar procurar muito longe do local do padrão para encontrar informações relevantes sobre o padrão.

Se usarmos sabiamente esses princípios, é possível generalizar/aprender boas representações com quantidade beeeeeeeeem menor de parâmetros!

Reorganizando a arquitetura da rede

Logo, para cada pixel na posição $(𝑖 ,𝑗)$

$$\begin{split}\begin{aligned} \left[\mathbf{H}\right]_{i, j} &= \sum_k \sum_l[\mathsf{W}]_{i, j, k, l} [\mathbf{X}]_{k, l} = \sum_a \sum_b [\mathsf{V}]_{i, j, a, b} [\mathbf{X}]_{i+a, j+b}.\end{aligned}\end{split}$$

Com $k = i+a$ e $l = j+b$, funcionando como uma re-indexação. Assim, $[\mathsf{V}]_{i, j, a, b} = [\mathsf{W}]_{i, j, i+a, j+b}$

1. Invariância translacional

Um deslocamento em $X$ deve levar a um deslocamento em $H$. Isso só é possível se $V$ não depender de $(i, j)$ $\to$ correção $[\mathsf{V}]_{i, j, a, b} = [\mathbf{V}]_{a, b}$.

$$[\mathbf{H}]_{i, j} = \sum_a\sum_b [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b}$$

Estamos ponderando (com $\mathbf{V}_{a, b}$) pixels na vizinhança de $(i, j)$ para obter $\mathbf{H}_{i, j}$. E observe que essa ponderação não depende mais da localização $(i, j)$ dentro da imagem, ou seja, redução grande parâmetros.

2. Localidade

O que estiver longe de uma certa vizinhança de $(i, j)$, não interessa. Ou seja, se $|a|> \Delta$ ou $|b|> \Delta$ $\implies \mathbf{V}_{a, b} = 0$. Sendo assim, finalmente, temos:

$$[\mathbf{H}]_{i, j} = \sum_{a = -\Delta}^{\Delta} \sum_{b = -\Delta}^{\Delta} [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b}.$$

Com esse tipo de operação na camada $H$, podemos dizer que ela é uma camada de CONVOLUÇÃO
... e $\mathbf{V}$ é um kernel de convolução (uma espécie de filtro).

OBS: função de convolução

Operação de somatório do produto entre duas funções, ao longo da região em que elas se sobrepõem, em razão do deslocamento existente entre elas.

fig02.png

Redes Neuronais Convolucionais - CNN

Redes formadas por camadas de convolução (ps: a nomenclatura correta seria correlação cruzada)

Fig03.png

$\mathbf{X} : n_h \times n_w$ (matriz de entrada)
$\mathbf{W} : k_h \times k_w$ (matriz de kernel)
$b : $ (bias)
$\mathbf{Y} : (n_h - k_h + 1) \times (n_w - k_w + 1)$
$$\mathbf{Y} = \mathbf{X} \mathbf{W} + b$$

Exemplos de kernels para transformações específicas

Fig04.png

Exemplo com muitas camadas

Fig05.png

Entrada: imagem 32 x 32
7 camadas com kernel 5 x 5
$\implies$
Saída 1: 28 x 28
...
Saída 7: 4 x 4

Para se evitar perdas de informações de borda $\to$ Padding

Fig06.png

Para acelerar redução da dimensionalidade $\to$ Stride

Fig07.png

Vários canais de entrada

Fig08.png

Fig09.png

$\mathbf{X} : c_i \times n_h \times n_w$ (entrada)
$\mathbf{W} : c_i \times k_h \times k_w$ (kernel)
$\mathbf{Y} : m_h \times m_w$
$$\mathbf{Y} = \sum_{i = 0}^{c_i}\mathbf{X_{i,:,:}} \mathbf{W_{i,:,:}}$$

Vários canais de saída

Podemos ter vários kernels 3D, cada um gerando um canal de saída.

$\mathbf{X} : c_i \times n_h \times n_w$ (entrada)
$\mathbf{W} : c_o \times c_i \times k_h \times k_w$ (kernel)
$\mathbf{Y} : c_o \times m_h \times m_w$
$\mathbf{Y_{i,:,:}} = \mathbf{X} \mathbf{W_{i,:,:,:}}$ para cada $i=1,...,c_o$

Intuitivamente, poderíamos olhar cada canal de saída como se correspondesse a conjuntos de diferentes características, ou seja, como se fosse capaz de reconhecer um padrão específico dentre muitos existentes.

Fig10.png

Escolha popular: kernel 1x1

$k_h = k_w = 1 \implies$ não reconhece padrões espaciais mas impacta no canal de saída realizando fusão.

Fig11.svg

Camada de Pooling

Camadas de convolução são sensíveis à posição. Ex: numa tarefa de detecção de borda da imagem, se deslocarmos a imagem 1 pixel para a direita (ou esquerda), a saída da convolução poderá perder a capacidade de detecção.

Fig12.png

Variações de luminosidade, escalas, rotações, etc, também poderiam trazer prejuízos ao reconhecimento do padrão.

Pooling de máximo e Pooling de média

Fig13.png

Fig14.png

Fig15.png

Rede LeNet-5

LeCun, Y., Bottou, L., Bengio, Y., Haffner, P., & others. (1998). Gradient-based learning applied to document recognition. Proceedings of the IEEE, 86(11), 2278–2324.

Objetivo: reconhecimento de digitos manuscritos de cheques depositados em terminais de auto-atendimento.

Fig18.png

Fig17.png

Arquitetura da rede

Fig16.svg

Fig19.svg

Exemplo: treinamento com MNIST Fashion

Resultado de execução com uma placa Tesla T4

Fig26.png

Obs: pequena adaptação para uso em GPU

train_ch6(..., d2l.try_gpu())

...
def try_gpu(i=0):
    #### Retorna gpu(i) se existir, senão returna cpu().
    return npx.gpu(i) if npx.num_gpus() >= i + 1 else npx.cpu()

Dados da mem. principal $\to$ mem. GPU

def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
    net.initialize(force_reinit=True, ctx=device, init=init.Xavier())
    loss = gluon.loss.SoftmaxCrossEntropyLoss()
    trainer = gluon.Trainer(net.collect_params(), 'sgd',
                            {'learning_rate': lr})
    ...
    for epoch in range(num_epochs):
        metric = d2l.Accumulator(3)
        for i, (X, y) in enumerate(train_iter):
            timer.start() 
            ### ====> 
            X, y = X.as_in_ctx(device), y.as_in_ctx(device)
            #########
            with autograd.record():
                y_hat = net(X)
                l = loss(y_hat, y)
            l.backward()
            ...          
        ### ====>    
        test_acc = evaluate_accuracy_gpu(net, test_iter)
        #########
        animator.add(epoch + 1, (None, None, test_acc))
    print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
          f'test acc {test_acc:.3f}')
    print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
          f'on {str(device)}')

def evaluate_accuracy_gpu(net, data_iter, device=None):  
    ### ===>
    if not device: 
        device = list(net.collect_params().values())[0].list_ctx()[0]
    #########
    metric = d2l.Accumulator(2)
    for X, y in data_iter:
        ### ====>    
        X, y = X.as_in_ctx(device), y.as_in_ctx(device)
        #########
        metric.add(d2l.accuracy(net(X), y), y.size)
    return metric[0] / metric[1]

mundo Antigo $\to$ 2010 $\to$ mundo Contemporâneo

mundo Antigo:

  1. Obter um bom conjunto de dados: tarefa difícil, pois exigia-se sensores caros
  2. Pré-processar o conjunto de dados para extrair manualmente características a partir de conhecimentos de ótica, geometria, etc...
  3. Aplicar algumas transformações de características através de extratores padrão conhecidos
  4. Aplicar os dados ao classificador favorito, normalmente um modelo linear ou método de kernel (ex: Support Vector Machine (SVM)

Hipótese a ser testada: a própria representação de características deveria ser aprendida durante o próprio treinamento e não confeccionada.

CNNs (ex LeNet) não decolaram devido a dois aspectos:

  1. Carência de dados: modelos profundos precisam de muuuuuuitos dados para superar modelos tradicionais
  2. Carência de hardware: quanto mais profundo, mais exigência de CPU/Memória

2010:

Fig21.png

Fig20.png

mundo Contemporâneo....

...Rede AlexNet

Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2012). Imagenet classification with deep convolutional neural networks. Advances in neural information processing systems (pp. 1097–1105).

Fig25.png

Principais modificações em relação ao LeNet:

Fig24.png

gluon.data.vision.transforms.RandomFlipLeftRight()
gluon.data.vision.transforms.RandomFlipTopBottom()
gluon.data.vision.transforms.RandomResizedCrop((200, 200), scale=(0.1, 1), ratio=(0.5, 2))
gluon.data.vision.transforms.RandomBrightness(0.5)
gluon.data.vision.transforms.RandomHue(0.5)
gluon.data.vision.transforms.RandomColorJitter(brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5)

Hipótese de que a própria representação de características deveria ser aprendida durante o próprio treinamento e não confeccionada foi TESTADA E VALIDADA!

Fig22.png

👑 AlexNet ganhou a competição ImageNet de 2012

Implementação da arquitetura da rede com mxnet.gluon

Fig23.svg

Exemplo: treinamento com MNIST Fashion...

... ao invés do ImageNet (que custa muuuuuuuuuuuuito mais tempo de execução)

Fashion-MNIST: 28×28 pixels
ImageNet: 224x224 pixels
$\implies$ resize=224 (prática não muito recomendada 🙈)

Resultado de execução com uma placa Tesla T4

Fig27.png

Ajudando a convergir

A fim de reconhecer padrões mais difíceis e/ou com maior acurácia, há uma tendência de se observar incremento do número de camadas da arquitetura de aprendizado profundo. Isso pode gerar dois grandes problemas: tempo e dificuldade de convergência. A questão é que quaisquer mudanças nas camadas mais básicas geram modificações em cadeia no restante da rede; últimas camadas precisam retreinar várias vezes pois o erro só é computado na última camada.

Fig29.png

Como minimizar a necessidade de grandes mudanças nas últimas camadas? Corrigindo a média e variância do minibatch de treinamento da iteração corrente para evitar modificações na distribuição de probabilidades observadas (similar ao covariate shift) $\to$ NORMALIZAÇÃO DE BATCH

$$\begin{split}\begin{aligned} \hat{\boldsymbol{\mu}}_\mathcal{B} &= \frac{1}{|\mathcal{B}|} \sum_{\mathbf{x} \in \mathcal{B}} \mathbf{x},\\ \hat{\boldsymbol{\sigma}}_\mathcal{B}^2 &= \frac{1}{|\mathcal{B}|} \sum_{\mathbf{x} \in \mathcal{B}} (\mathbf{x} - \hat{\boldsymbol{\mu}}_{\mathcal{B}})^2 + \epsilon.\end{aligned}\end{split}$$

e o novo $\mathbf{x}$:

$$\mathrm{BN}(\mathbf{x}) = \boldsymbol{\gamma} \odot \frac{\mathbf{x} - \hat{\boldsymbol{\mu}}_\mathcal{B}}{\hat{\boldsymbol{\sigma}}_\mathcal{B}} + \boldsymbol{\beta}.$$

onde $\boldsymbol{\gamma}$ e $\boldsymbol{\beta}$ são novos parâmetros (escala e deslocamento), que também são aprendidos.

Assim, as magnitudes das variáveis nas camadas intermediárias não divergem durante o treinamento.

Uso:

Aumentar o número de camadas aumenta acurácia?

Queremos $f^*_\mathcal{F} \stackrel{\mathrm{def}}{=} \mathop{\mathrm{argmin}}_f L(\mathbf{X}, \mathbf{y}, f) \text{ referente a } f \in \mathcal{F}$ (sendo $\mathcal{F}$ uma arquitetura de rede específica).

Uma outra arquitetura de rede maior e mais complexa, $\mathcal{F}'$, só garantirá a possibilidade de uma acurácia ainda melhor se $\mathcal{F} \subseteq \mathcal{F}'$ (lado direito da figura abaixo).

Fig28.svg

Usualmente, ao se adicionar uma nova camada muda-se a classe de função e, portanto, caimos na situação à esquerda da imagem anterior. Como garantir que permaneçamos na situação à direita?

Resposta: treinar a nova camada com a função identidade $\implies$ a camada corrente deve aprender o mapeamento residual...

...BLOCO RESIDUAL

Fig30.svg

Observe que a existência de um bloco residual $\implies$ existência de uma conexão direta (atalho - skip) com a camada seguinte.

Na prática, a ideia do bloco residual é adicionar dinamicidade à arquitetura de uma rede neuronal profunda, permitindo que a própria rede ajuste o número de camadas de forma otimizada durante o treinamento.

Como assim?

Em geral, não sabemos o número ideal de camadas necessárias para uma rede neural, o que pode depender da complexidade do conjunto de dados. Em vez de tratar o número de camadas como um hiperparâmetro, o uso dos skips permite que a própria rede aprenda a saltar o treinamento das camadas que não se mostram úteis e não agregam valor à acurácia geral.

... Rede ResNet

He, K., Zhang, X., Ren, S., & Sun, J. (2016). Deep residual learning for image recognition. Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 770–778)

Arquitetura ResNet-18

Fig32.svg

... onde cada módulo residual contém 2 tipos de blocos residuais:

Fig31.svg

OBS: convolução com kernel 1x1 é aplicada para se ajustar canais de saída e resolução.

  1. 1 camada convolucional
  2. 1 módulo residual inicial contendo 2 blocos residuais cada (apenas de um tipo: sem kernel 1x1) = 4 camadas convolucionais (2 por bloco)
  3. 3 módulos residuais seguintes contendo 2 blocos residuais (dos dois tipos) = 12 camadas convolucionais (2 por bloco)
  4. 1 camada completamente conectada final (MLP)
    TOTAL = 18 camadas.

👑 ResNet ganhou a competição ImageNet de 2015

Resultado de execução em uma placa Tesla T4

Fig34.png

Referências para estudo

Cap. 6, Seção 7.1 e 7.6 de Dive into Deep Learning

Sobre data augmentation citado aqui: ver 13.1.1 de [Dive into Deep Learning]